﻿using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using Monti.MVVM;
using System;

namespace SqliteHelper.Classes
{
   /// <summary>
   /// View model for main window.
   /// </summary>
   public class MainViewModel : MainWindowViewModelBase
   {
      #region Fields

      private string m_TxtStatus;
      private RelayCommand m_CmdNewDatabase;
      private RelayCommand m_CmdOpenDatabase;
      private RelayCommand m_CmdSaveDatabase;
      private RelayCommand m_CmdSaveAllDatabase;
      private RelayCommand m_CmdUndoDatabase;
      private RelayCommand m_CmdCloseDatabase;
      private RelayCommand m_CmdAddTable;
      private RelayCommand m_CmdCopyTable;
      private RelayCommand m_CmdDropTable;
      private RelayCommand m_CmdExecuteSql;
      private RelayCommand m_CmdDrop;
      private SqliteDatabase m_SelectedDatabase;
      private bool m_BoolRunSqlOnAllDatabases;
      private string m_TxtSqlResult;
      private string m_SqlText;

      #endregion

      #region Setup

      /// <summary>
      /// Initializes a new main view model.
      /// </summary>
      public MainViewModel ()
      {
         this.StatusText = "Ready";
         this.DialogVisibility = Visibility.Collapsed;
         this.DatabaseList = new ObservableCollection <SqliteDatabase> ();
         DialogService.Instance.AddService (new FileDialogService ());
         DialogService.Instance.AddService (new MessageBoxDialogService ());
         this.ClosingAction = obj => { foreach (var database in this.DatabaseList) database.Dispose (); };
      }

      #endregion

      #region Properties

      /// <summary>
      /// Gets the list of open databases.
      /// </summary>
      public ObservableCollection<SqliteDatabase> DatabaseList { get; private set; }

      /// <summary>
      /// Gets or Sets the text to display in the status bar.
      /// </summary>
      public string StatusText
      {
         get { return this.m_TxtStatus; }
         set
         {
            this.m_TxtStatus = value;
            this.RaisePropertyChanged ("StatusText");
         }
      }

      /// <summary>
      /// Gets or Sets if we should run the sql command on all databases.
      /// </summary>
      public bool RunSqlOnAllDatabases
      {
         get { return this.m_BoolRunSqlOnAllDatabases; }
         set
         {
            this.m_BoolRunSqlOnAllDatabases = value;
            this.RaisePropertyChanged ("RunSqlOnAllDatabases");
         }
      }

      /// <summary>
      /// Gets or Sets the result text of the sql code.
      /// </summary>
      public string SqlResult
      {
         get { return this.m_TxtSqlResult; }
         set
         {
            this.m_TxtSqlResult = value;
            this.RaisePropertyChanged ("SqlResult");
         }
      }

      /// <summary>
      /// Gets or Sets the text in the sql input box.
      /// </summary>
      public string SqlText
      {
         get { return this.m_SqlText; }
         set
         {
            this.m_SqlText = value;
            this.RaisePropertyChanged ("SqlText");
         }
      }

      /// <summary>
      /// Gets or Sets the selected database.
      /// </summary>
      public SqliteDatabase SelectedDatabase
      {
         get { return this.m_SelectedDatabase; }
         set
         {
            this.m_SelectedDatabase = value;
            this.RaisePropertyChanged ("SelectedDatabase");
         }
      }

      /// <summary>
      /// Gets the command to create a new database.
      /// </summary>
      public ICommand NewDatabaseCommand
      {
         get { return this.m_CmdNewDatabase ?? (this.m_CmdNewDatabase = new RelayCommand (NewDatabase)); }
      }

      /// <summary>
      /// Gets the command to open a database.
      /// </summary>
      public ICommand OpenDatabaseCommand
      {
         get { return this.m_CmdOpenDatabase ?? (this.m_CmdOpenDatabase = new RelayCommand (OpenDatabase)); }
      }

      /// <summary>
      /// Gets the command to save a database.
      /// </summary>
      public ICommand SaveDatabaseCommand
      {
         get { return this.m_CmdSaveDatabase ?? (this.m_CmdSaveDatabase = new RelayCommand (SaveDatabase, CanSaveDatabase)); }
      }

      /// <summary>
      /// Gets the command to save a database.
      /// </summary>
      public ICommand SaveAllDatabaseCommand
      {
         get { return this.m_CmdSaveAllDatabase ?? (this.m_CmdSaveAllDatabase = new RelayCommand (SaveAllDatabase, CanSaveAllDatabase)); }
      }

      /// <summary>
      /// Gets the command to undo database transaction.
      /// </summary>
      public ICommand UndoDatabaseCommand
      {
         get { return this.m_CmdUndoDatabase ?? (this.m_CmdUndoDatabase = new RelayCommand (UndoDatabase, CanSaveDatabase)); }
      }

      /// <summary>
      /// Gets the command to close the database.
      /// </summary>
      public ICommand CloseDatabaseCommand
      {
         get { return this.m_CmdCloseDatabase ?? (this.m_CmdCloseDatabase = new RelayCommand (CloseDatabase, IsSelectedDatabaseValid)); }
      }

      /// <summary>
      /// Gets the command to add a new table.
      /// </summary>
      public ICommand NewTableCommand
      {
         get { return this.m_CmdAddTable ?? (this.m_CmdAddTable = new RelayCommand (AddNewTable, IsSelectedDatabaseValid)); }
      }

      /// <summary>
      /// Gets the command to copy a new table.
      /// </summary>
      public ICommand CopyTableCommand
      {
         get { return this.m_CmdCopyTable ?? (this.m_CmdCopyTable = new RelayCommand (CopyTable, CanCopyTable)); }
      }

      /// <summary>
      /// Gets the command drop the selected table.
      /// </summary>
      public ICommand DropTableCommand
      {
         get { return this.m_CmdDropTable ?? (this.m_CmdDropTable = new RelayCommand (DropTable, CanDropTable)); }
      }

      /// <summary>
      /// Gets the command to execute sql.
      /// </summary>
      public ICommand ExecuteSqlCommand
      {
         get { return this.m_CmdExecuteSql ?? (this.m_CmdExecuteSql = new RelayCommand (ExecuteSql, IsSelectedDatabaseValid)); }
      }

      /// <summary>
      /// Gets the command when a drop occurs.
      /// </summary>
      public ICommand DropCommand
      {
         get { return this.m_CmdDrop ?? (this.m_CmdDrop = new RelayCommand (Drop)); }
      }

      /// <summary>
      /// Gets an action that occurs when the window is closing.
      /// </summary>
      public Action<object> ClosingAction { get; private set; }

      #endregion

      #region Methods

      /// <summary>
      /// Adds a new database to the list of open databases.
      /// </summary>
      /// <param name="pathToDatabase">Path to database to add.</param>
      /// <returns>True if successful, otherwise false.</returns>
      private bool AddDatabase (string pathToDatabase)
      {
         SqliteDatabase database;
         try
         {
            database = new SqliteDatabase {FilePath = pathToDatabase, IsSelected = true};
         }
         catch (Exception)
         {
            return false;
         }
         this.DatabaseList.Add (database);
         return true;
      }

      /// <summary>
      /// Removes the given database from the list.
      /// </summary>
      /// <param name="database">Datatbase to remove.</param>
      internal void RemoveDatabase (SqliteDatabase database)
      {
         this.DatabaseList.Remove (database);
      }

      /// <summary>
      /// Prompts the user to open a database.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void OpenDatabase (object obj)
      {
         this.StatusText = "Opening database";
         var fileService = DialogService.Instance.GetService<FileDialogService> ();
         var viewModel = new FileDialogViewModel { AllowMultipleSelection = true, Filter = "Database files (*.db)|*.db|All files(*.*)|*.*" };
         bool? result = fileService.OpenFile (viewModel);
         if (result.HasValue && result.Value)
         {
            foreach (var file in viewModel.FileNames)
               this.AddDatabase (file);
         }
         this.StatusText = "Ready";
      }

      /// <summary>
      /// Checks if the Close database command can execute.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private bool IsSelectedDatabaseValid (object obj)
      {
         return this.SelectedDatabase != null;
      }

      /// <summary>
      /// Closes the database.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void CloseDatabase (object obj)
      {
         this.SelectedDatabase.Dispose ();
         this.RemoveDatabase (this.SelectedDatabase);
      }

      /// <summary>
      /// Prompts the user to create a new database.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void NewDatabase (object obj)
      {
         var content = new NewDatabaseViewModel ();
         content.Closing += NewDatabaseClosing;
         this.DialogContent = content;
      }

      /// <summary>
      /// Occurs when the new database dialog is closing.
      /// </summary>
      /// <param name="dialog">Dialog base that fired the closing event.</param>
      private void NewDatabaseClosing (DialogBase dialog)
      {
         var newDatabaseDialog = dialog as NewDatabaseViewModel;
         if (newDatabaseDialog != null && dialog.DialogCloseAction == DialogCloseAction.Ok)
         {
            dialog.OkToClose = this.AddDatabase (newDatabaseDialog.DatabasePath);
            if (!dialog.OkToClose)
               dialog.ErrorText = "Unable to create database!";
         }
      }

      /// <summary>
      /// Checks to see if the save command can be executed.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      /// <returns></returns>
      private bool CanSaveDatabase (object obj)
      {
         return this.SelectedDatabase != null && this.SelectedDatabase.NeedsSaving;
      }

      /// <summary>
      /// Saves the current database.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void SaveDatabase (object obj)
      {
         this.SelectedDatabase.Save ();
      }

      /// <summary>
      /// Checks to see if the save all command can be executed.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      /// <returns></returns>
      private bool CanSaveAllDatabase (object obj)
      {
         return this.DatabaseList.FirstOrDefault(database => database.NeedsSaving) != null;
      }

      /// <summary>
      /// Saves the all databases.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void SaveAllDatabase (object obj)
      {
         this.StatusText = "Saving";
         foreach (var database in this.DatabaseList.Where (database => database.NeedsSaving))
            database.Save ();
         this.StatusText = "Ready";
      }

      /// <summary>
      /// Undoes the transaction in current database.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void UndoDatabase (object obj)
      {
         this.StatusText = "Rolling back changes";
         this.SelectedDatabase.UndoChanges ();
         this.StatusText = "Ready";
      }

      /// <summary>
      /// Adds a new table.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void AddNewTable (object obj)
      {
         var dialog = new SqliteTableViewModel ();
         dialog.Closing += NewTableClosing;
         this.DialogContent = dialog;
      }

      /// <summary>
      /// Occurs when the new table dialog is closing.
      /// </summary>
      /// <param name="dialog">Dialog base that fired the closing event.</param>
      private void NewTableClosing (DialogBase dialog)
      {
         var newTableDialog = dialog as SqliteTableViewModel;
         if (newTableDialog != null && dialog.DialogCloseAction == DialogCloseAction.Ok)
         {
            string error = this.SelectedDatabase.AddNewTable (newTableDialog.Name, newTableDialog.Columns);
            if (!string.IsNullOrEmpty (error))
            {
               dialog.OkToClose = false;
               dialog.ErrorText = error;
            }
         }
      }

      /// <summary>
      /// Checks if we can copy a table.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      /// <returns>If command can execute or not.</returns>
      private bool CanCopyTable (object obj)
      {
         return this.DatabaseList.Count > 0;
      }

      /// <summary>
      /// Copies a table.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void CopyTable (object obj)
      {
         this.DialogContent = new CopyTableViewModel (this);
      }

      /// <summary>
      /// Checks if we can drop a table.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      /// <returns>If command can execute or not.</returns>
      private bool CanDropTable (object obj)
      {
         return this.SelectedDatabase != null && this.SelectedDatabase.SelectedTable != null;
      }

      /// <summary>
      /// Drops a table.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void DropTable (object obj)
      {
         this.SelectedDatabase.DropSelectedTable ();
      }

      /// <summary>
      /// Occurs when a drop happens.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void Drop (object obj)
      {
         var handlerData = obj as HandlerData;
         if (handlerData != null)
         {
            var eventArgs = handlerData.EventArgs as DragEventArgs;
            if (eventArgs != null)
            {
               if (eventArgs.Data.GetDataPresent (DataFormats.FileDrop))
               {
                  var droppedFiles = eventArgs.Data.GetData (DataFormats.FileDrop, true) as string[];
                  if (droppedFiles != null)
                  {
                     foreach (var file in droppedFiles)
                        this.AddDatabase (file);
                  }
               }
            }
         }
      }

      /// <summary>
      /// Executes the execute sql command.
      /// </summary>
      /// <param name="obj">Command parameter.</param>
      private void ExecuteSql (object obj)
      {
         this.StatusText = "Running SQL";
         var errors = string.Empty;
         errors = this.RunSqlOnAllDatabases ?
            this.DatabaseList.Aggregate (errors, (current, database) => current + database.ExecuteSql (this.SqlText))
            : this.SelectedDatabase.ExecuteSql (this.SqlText);
         this.SelectedDatabase.RefreshData ();
         this.SqlResult = string.IsNullOrEmpty (errors) ? "SQL executed successfully!" : errors;
         this.StatusText = "Ready";
      }

      #endregion
   }
}
